#include <cstdlib>
#include <iostream>

#include "Framework/Win32/Win32Framework.h"
#include "Framework/App.h"

#include "GfxDriver/GfxDriver.h"
#include "GfxDriver/Shader.h"
#include "GfxDriver/Texture.h"
#include "GfxDriver/VertexBuffer.h"
#include "GfxDriver/IndexBuffer.h"
#include "GfxDriver/VertexInput.h"

#include "Image/ImageService.h"
#include "Image/Image.h"

#include "Skygen/PRS.h"
#include "Skygen/StaticSkyboxGenerator.h"
#include "Skygen/DynamicCloudSkyboxGenerator.h"
#include "Skygen/SphericalMapSkyboxGenerator.h"
#include "Skygen/TilingPerlinNoise.h"

extern "C" void glutSolidSphere(double radius, int slices, int stacks);

namespace Orca {

	const uint32 cSegmentCnt = 32;

	//--------------------------------------------------------------------------
	class TSkygenApp : public IApp {

		TextureCube		*mSkyCubeTex;
		Texture2d			*mTerrainNoiseTex;
		Texture2d			*mTerrainDiffTex1;
		Texture2d			*mTerrainDiffTex2;

		VertexProgram		*mSkyboxVProg;
		FragmentProgram	*mSkyboxFProg;
		Shader				*mSkyboxShader;
		VertexBuffer		*mSkyboxVB;
		IndexBuffer		*mSkyboxIB;
		VertexInput		*mSkyboxVI;

		VertexProgram		*mTerrainVProg;
		FragmentProgram	*mTerrainFProg;
		Shader				*mTerrainShader;
		VertexBuffer		*mTerrainVB;
		IndexBuffer		*mTerrainIB;
		VertexInput		*mTerrainVI;

		ISkyboxGenerator	*mSkygen;

		PRS mWorldXform, mViewXform;

		bool mMouseBtnDn[3];
		int mMouseDnX, mMouseDnY;
		int mLastDX, mLastDY;

	public:

		//------------------------------
		TSkygenApp() {

			mMouseBtnDn[0] = mMouseBtnDn[1] = mMouseBtnDn[2] = false;
		}

		//------------------------------
		virtual ~TSkygenApp() {

		}

		//------------------------------
		virtual void getAppTitle(Str &title) {
			title = "Skygen";
		}

		//------------------------------
		virtual void getInitRes(uint32 &width, uint32 &height) {
			width = 640;
			height = 480;
		}

		//------------------------------
		virtual void registerPluginModules() {
		}

		//------------------------------
		virtual bool startup(Framework *framework) {

			//mSkygen = new TStaticSkyboxGenerator;
			//mSkygen = new TDynamicCloudSkyboxGenerator;
			mSkygen = new TSphericalMapSkyboxGenerator;

			//ORCA_USE_SERVICE(GfxDriver);

			INST<GfxDriver>()->setDepthTestEnable(true);
			INST<GfxDriver>()->setDepthTestFunc(CMP_LESS_EQUAL);

			_createSkyboxTexture();
			
			_createSkyboxGeom();

			_createTerrainTexture();

			_createTerrainGeom();

			_createVertexInput();

			_createShader(framework);

			mViewXform.move( vec3(0, 0, 10) );

			return true;
		}

		//------------------------------
		virtual void shutdown() {

			delete mTerrainVI;
			delete mTerrainIB;
			delete mTerrainVB;

			delete mSkyboxVI;
			delete mSkyboxIB;
			delete mSkyboxVB;
			
			delete mSkyCubeTex;
			delete mTerrainNoiseTex;
			delete mTerrainDiffTex1;
			delete mTerrainDiffTex2;

			delete mSkyboxShader;
			delete mSkyboxVProg;
			delete mSkyboxFProg;

			delete mTerrainShader;
			delete mTerrainVProg;
			delete mTerrainFProg;

			delete mSkygen;

		}

		//------------------------------
		virtual void step(float dt) {

			//ORCA_USE_SERVICE(GfxDriver);

			INST<GfxDriver>()->clearFrameBuf( vec4(0.05f, 0.1f, 0.2f, 0.0f) );
			INST<GfxDriver>()->clearDepthBuf(1.0f);

			INST<GfxDriver>()->beginRender();

			
			INST<GfxDriver>()->setViewTransform( invert(mViewXform.getMtx4()) );

			mSkygen->step(dt);
			mSkygen->updateShaderConstants(mSkyboxShader);
			
			// large sphere
			mWorldXform.mScale.set(1.0f, 1.0f, 1.0f);
			INST<GfxDriver>()->setWorldTransform( mWorldXform.getMtx4() );

			INST<GfxDriver>()->setCullMode(CULL_CCW);	// render backface

			if(mSkyboxShader) mSkyboxShader->beginRender();

			if(mSkyboxVB) {

				INST<GfxDriver>()->beginDrawing(mSkyboxVI);

				INST<GfxDriver>()->drawIndexed(PT_TRIANGLE_LIST, 0, cSegmentCnt * cSegmentCnt * 2);
				//INST<GfxDriver>()->draw(PT_POINT_LIST, 0, (cSegmentCnt+1) * (cSegmentCnt+1));

				INST<GfxDriver>()->endDrawing();
			}

			if(mSkyboxShader) mSkyboxShader->endRender();
			
			// smaller sphere
			INST<GfxDriver>()->setCullMode(CULL_CW);	// render front-face

			mWorldXform.mScale.set(0.001f, 0.001f, 0.001f);
			INST<GfxDriver>()->setWorldTransform( mWorldXform.getMtx4() );

			if(mSkyboxShader) mSkyboxShader->beginRender();

			if(mSkyboxVB) {

				INST<GfxDriver>()->beginDrawing(mSkyboxVI);

				INST<GfxDriver>()->drawIndexed(PT_TRIANGLE_LIST, 0, cSegmentCnt * cSegmentCnt * 2);
				//INST<GfxDriver>()->draw(PT_POINT_LIST, 0, (cSegmentCnt+1) * (cSegmentCnt+1));

				INST<GfxDriver>()->endDrawing();
			}

			if(mSkyboxShader) mSkyboxShader->endRender();

			// terrain
			INST<GfxDriver>()->setCullMode(CULL_CW);	// render front-face

			INST<GfxDriver>()->setWorldTransform( identity4() );

			if(mTerrainShader) mTerrainShader->beginRender();

			if(mTerrainVB) {

				INST<GfxDriver>()->beginDrawing(mTerrainVI);

				INST<GfxDriver>()->drawIndexed(PT_TRIANGLE_LIST, 0, cSegmentCnt * cSegmentCnt * 2);
				//INST<GfxDriver>()->draw(PT_POINT_LIST, 0, (cSegmentCnt+1) * (cSegmentCnt+1));

				INST<GfxDriver>()->endDrawing();
			}
			
			if(mTerrainShader) mTerrainShader->endRender();

			// finish rendering
			INST<GfxDriver>()->endRender();
		}

		//------------------------------
		virtual void notifyResChanged(uint32 width, uint32 height) {

			const float zNear = 0.01f;
			const float zFar = 2000.0f;
			const float aspect = (float)width / (float)height;
			const float fovy = gmtl::Math::PI_OVER_4;

			mtx44 projMtxP = identity4();

			float f = 1.0f / tanf( fovy * 0.5f );
			float inv_dist = 1.0f / (zNear - zFar);

			projMtxP[0][0] = f / aspect;
			projMtxP[1][1] = f;
			projMtxP[2][2] = (zFar + zNear) * inv_dist;
			projMtxP[2][3] = (2.0f * zFar * zNear) * inv_dist;
			projMtxP[3][2] = -1.0f;

			INST<GfxDriver>()->setProjTransform(projMtxP);
			INST<GfxDriver>()->setViewport(0, 0, width, height);
		}

		//------------------------------
		virtual void notifyKeyboardEvent(uint8 c) {

			const float speed = Math::PI / 180.0f;

			switch(c) {

				/*
				case 'w': case 'W':
				{
					mWorldXform.pitch(speed);
					break;
				}

				case 's': case 'S':
				{
					mWorldXform.pitch(-speed);
					break;
				}

				case 'd': case 'D':
				{
					mWorldXform.yaw(speed);
					break;
				}

				case 'a': case 'A':
				{
					mWorldXform.yaw(-speed);
					break;
				}
				*/

				case 'w': case 'W':
				{
					mViewXform.move( vec3(0.0f, 0.0f, -1.0f) );
					break;
				}

				case 's': case 'S':
				{
					mViewXform.move( vec3(0.0f, 0.0f,  1.0f) );
					break;
				}
			}
		}

		//------------------------------
		virtual void notifyMouseButtonEvent(MOUSE_BUTTON btn, bool down, int x, int y) {

			mMouseBtnDn[btn] = down;

			if(mMouseBtnDn[IApp::LEFT_BUTTON]) {

				mMouseDnX = x;
				mMouseDnY = y;

				mLastDX = mLastDY = 0;
			}
			else {
			}
		}

		//------------------------------
		virtual void notifyMouseMotionEvent(int x, int y) {

#define DEG2RAD(x) x * Math::PI / 180.0f
			
			int dx = x - mMouseDnX;
			int dy = y - mMouseDnY;

			float motionX = (dx - mLastDX) * 0.2f;
			float motionY = (dy - mLastDY) * 0.2f;

			if(mMouseBtnDn[IApp::LEFT_BUTTON]) {

				mViewXform.yaw( -DEG2RAD(motionX) );
				mViewXform.pitch( -DEG2RAD(motionY) );
			}
			else if(mMouseBtnDn[IApp::RIGHT_BUTTON]) {

				mViewXform.move( vec3(0.0f, 0.0f,  motionY) * 0.5f );
			}
			else if(mMouseBtnDn[IApp::MIDDLE_BUTTON]) {

				mViewXform.move( vec3(-motionX, motionY,  0.0f) * 0.5f );
			}
			
			mLastDX = dx;
			mLastDY = dy;

#undef DEG2RAD

		}

		void _createTerrainTexture() {

			const uint32 cTexSize = 256;

			mTerrainNoiseTex = INST<GfxDriver>()->create2DTexture(
				cTexSize, cTexSize, PF_INT8_RGBA);

			if(mTerrainNoiseTex) {

				uint8 *data = (uint8*)mTerrainNoiseTex->requestUpload();

				Image *img = INST<ImageService>()->createImage("RGBA8", cTexSize, cTexSize);

				uint8 *imgData = (uint8*)img->getDataPtr();

				if(data) {

					uint32 tilingSize = cTexSize / 16;
					TTilingPerlinNoise<float> pn( (int)tilingSize );

					float stepSize = ((float)tilingSize / cTexSize);

					float u = 0.0f, v = 0.0f;

					for(uint32 y=0; y<cTexSize; ++y) {

						u = 0.0f;

						for(uint32 x=0; x<cTexSize; ++x) {

							float n = 0.0f;
							
							n += pn.pnoise( u, v, 0.0f );
							n += pn.pnoise( u * 2.f, v * 2.f, 0.0f ) * .5f;
							n += pn.pnoise( u * 4.f, v * 4.f, 0.0f ) * .25f;
							n += pn.pnoise( u * 8.f, v * 8.f, 0.0f ) * .125f;
							
							//n = Math::abs(n);
							n = Math::clamp(n, -1.0f, 1.0f);
							n = n * 0.5f + 0.5f;

							uint8 val = uint8(n * 255.0f);
							
							*data++ = val;
							*data++ = val;
							*data++ = val;
							*data++ = 255;

							*imgData++ = val;
							*imgData++ = val;
							*imgData++ = val;
							*imgData++ = 255;

							u += stepSize;
						}

						v += stepSize;
					}
				}

				mTerrainNoiseTex->uploadToGPU(false);

				img->saveToFile("Skygen/output/terrainNoise.tga");

				Image *img1 = INST<ImageService>()->createImageFromFile("Skygen/texture/baseColor1.png");
				mTerrainDiffTex1 = img1->create2DTexture(false);
				delete img1;

				Image *img2 = INST<ImageService>()->createImageFromFile("Skygen/texture/baseColor2.png");
				mTerrainDiffTex2 = img2->create2DTexture(false);
				delete img2;
			}
		}

		//------------------------------
		void _createSkyboxTexture() {

			int fullSize = 256;
			int halfSize = 128;

			uint32 totalTime = 0;

			mSkyCubeTex = INST<GfxDriver>()->createCubeTexture( (uint32)fullSize, PF_INT8_RGBA);
			
			if(mSkyCubeTex) {

				for(uint32 side = 0; side < 6; ++side) {

					uint32 startTime = Kernel::inst()->getCurrentTime();

					Image *img = INST<ImageService>()->createImage("RGBA8", fullSize, fullSize);
				
					uint8 *dataPtr = (uint8*)mSkyCubeTex->requestUpload( (CubeMapSide)side );

					uint8 *imgPtr = (uint8*)img->getDataPtr();

					uint32 idx = 0;

					for(int y = 0; y < fullSize; ++y) {
						for(int x = 0; x < fullSize; ++x) {

							vec3 dir;

							float u = (float)x - halfSize + 0.5f;
							float v = (float)y - halfSize + 0.5f;
							
							switch(side) {
								case CUBE_POSITIVE_X:
								{
									dir.set( (float)halfSize, -v, -u);
									break;
								}

								case CUBE_NEGATIVE_X:
								{
									dir.set(-(float)halfSize, -v,  u);
									break;
								}

								case CUBE_POSITIVE_Y:
								{
									dir.set( u, (float)halfSize,  v);
									break;
								}

								case CUBE_NEGATIVE_Y:
								{
									dir.set( u, -(float)halfSize, -v);
									break;
								}

								case CUBE_POSITIVE_Z:
								{
									dir.set( u, -v, (float)halfSize);
									break;
								}

								case CUBE_NEGATIVE_Z:
								{
									dir.set(-u, -v, -(float)halfSize);
									break;
								}
							}

							normalize(dir);

							vec4 clr = mSkygen->computeSkyboxColor(dir, fullSize);

							uint8 
								r = (uint8)(clr[0] * 255.0f),
								g = (uint8)(clr[1] * 255.0f),
								b = (uint8)(clr[2] * 255.0f),
								a = (uint8)(clr[3] * 255.0f);

							*dataPtr++ = r;
							*dataPtr++ = g;
							*dataPtr++ = b;
							*dataPtr++ = a;

							*imgPtr++ = r;
							*imgPtr++ = g;
							*imgPtr++ = b;
							*imgPtr++ = a;
						}
					}

					mSkyCubeTex->uploadToGPU(false);

					totalTime += Kernel::inst()->getCurrentTime() - startTime;

					img->saveToFile("Skygen/output/Skybox" + toStr(side) + ".tga");
					delete img;
				}
				
				printf("Texture CUBE created in %.3f seconds\n", totalTime / 1000.0f);
			}
		}

		//------------------------------
		void _createSkyboxGeom() {

			const uint32 vertexPitch = cSegmentCnt + 1;
			const float inc = 1.0f / cSegmentCnt;
			const float radius = 2048.0f;

			// vertex buffers
			mSkyboxVB = INST<GfxDriver>()->createVertexBuffer(
				 sizeof(float) * 5 * vertexPitch * vertexPitch
				,true);

			if(mSkyboxVB) {

				float* vert = (float*)mSkyboxVB->requestUpload(true);

				if(vert) {

					float u = 0.0f;
					float v = 0.0f;

					for(uint32 y = 0; y < vertexPitch; ++y) {

						u = 0.0f;
						
						float phi = v * Math::PI;							// [0, 180], polar angle
						float cPhi = Math::cos(phi);
						float sPhi = Math::sin(phi);

						for(uint32 x = 0; x < vertexPitch; ++x) {
							
							float theta = u * (2.0f * Math::PI);			// [0, 360]
							float cTheta = Math::cos(theta);
							float sTheta = Math::sin(theta);

							vec3 dir(
								sPhi * cTheta,
								sPhi * sTheta,
								cPhi);

							normalize(dir);

							vec3 pos = dir * radius;

							*vert++ = pos[0];
							*vert++ = pos[2];
							*vert++ =-pos[1];
							*vert++ = u;
							*vert++ = v;
							
							u += inc;
						}

						v += inc;
					}
					
					mSkyboxVB->uploadToGPU();

					printf("skybox vertex buffer created!\n");
				}
			}

			// index buffer
			mSkyboxIB = INST<GfxDriver>()->createIndexBuffer(
				sizeof(uint16) * cSegmentCnt * cSegmentCnt * 6,
				true);

			if(mSkyboxIB) {

				uint16 *idx = (uint16*)mSkyboxIB->requestUpload(true);

				if(idx) {

					for(uint32 y=0; y<cSegmentCnt; ++y) {
						for(uint32 x=0; x<cSegmentCnt; ++x) {

							uint16 thisRow = (uint16)(x + y * vertexPitch);
							uint16 nextRow = (uint16)(x + (y+1) * vertexPitch);

							uint16 v[4] = {
								thisRow, 
								nextRow, 
								thisRow + 1,
								nextRow + 1};

							*idx++ = v[0];
							*idx++ = v[1];
							*idx++ = v[2];

							*idx++ = v[3];
							*idx++ = v[2];
							*idx++ = v[1];
						}
					}

					mSkyboxIB->uploadToGPU();

					printf("skybox index buffer created!\n");
				}
			}
		}

		//------------------------------
		void _createTerrainGeom() {

			const uint32 vertexPitch = cSegmentCnt + 1;
			const float inc = 1.0f / cSegmentCnt;
			const float radius = 512.0f;

			// vertex buffers
			mTerrainVB = INST<GfxDriver>()->createVertexBuffer(
				 sizeof(float) * 5 * vertexPitch * vertexPitch
				,true);

			if(mTerrainVB) {

				float* vert = (float*)mTerrainVB->requestUpload(true);

				if(vert) {

					float u = 0.0f;
					float v = 0.0f;

					for(uint32 y = 0; y < vertexPitch; ++y) {

						u = 0.0f;
						
						for(uint32 x = 0; x < vertexPitch; ++x) {
							
							*vert++ = (u - 0.5f)* radius;
							*vert++ = 0.0f;
							*vert++ = (v - 0.5f) * radius;
							*vert++ = u;
							*vert++ = v;
							
							u += inc;
						}

						v += inc;
					}
					
					mTerrainVB->uploadToGPU();

					printf("terrain vertex buffer created!\n");
				}
			}

			// index buffer
			mTerrainIB = INST<GfxDriver>()->createIndexBuffer(
				sizeof(uint16) * cSegmentCnt * cSegmentCnt * 6,
				true);

			if(mTerrainIB) {

				uint16 *idx = (uint16*)mTerrainIB->requestUpload(true);

				if(idx) {

					for(uint32 y=0; y<cSegmentCnt; ++y) {
						for(uint32 x=0; x<cSegmentCnt; ++x) {

							uint16 thisRow = (uint16)(x + y * vertexPitch);
							uint16 nextRow = (uint16)(x + (y+1) * vertexPitch);

							uint16 v[4] = {
								thisRow, 
								nextRow, 
								thisRow + 1,
								nextRow + 1};

							*idx++ = v[0];
							*idx++ = v[1];
							*idx++ = v[2];

							*idx++ = v[3];
							*idx++ = v[2];
							*idx++ = v[1];
						}
					}

					mTerrainIB->uploadToGPU();

					printf("terrain index buffer created!\n");
				}
			}
		}
		
		//------------------------------
		void _createVertexInput() {

			mSkyboxVI = INST<GfxDriver>()->createVertexInput();

			mSkyboxVI->append( mSkyboxVB, 0, sizeof(float) * 5 ) << VE_XYZ() << VE_TEXCOORD2(0);
			mSkyboxVI->append( mSkyboxIB, 0, IDT_16 );


			mTerrainVI = INST<GfxDriver>()->createVertexInput();

			mTerrainVI->append( mTerrainVB, 0, sizeof(float) * 5 ) << VE_XYZ() << VE_TEXCOORD2(0);
			mTerrainVI->append( mTerrainIB, 0, IDT_16 );
		}
		
		//------------------------------
		void _createShader(Framework *framework) {

			// shader
			uint8 *vpData; uint32 vpDataLength;
			uint8 *fpData; uint32 fpDataLength;
			
			// skybox shader
			INST<FileService>()->readFile(mSkygen->getVertexProgramFilename(), vpData, vpDataLength);
			INST<FileService>()->readFile(mSkygen->getFragmentProgramFilename(), fpData, fpDataLength);

			mSkyboxVProg = INST<GfxDriver>()->createVertexProgram(vpData);
			mSkyboxFProg = INST<GfxDriver>()->createFragmentProgram(fpData);
			mSkyboxShader = INST<GfxDriver>()->createShader(mSkyboxVProg, mSkyboxFProg);

			if(mSkyboxShader) {
				
				mSkyboxShader->beginBinding();

				mSkyboxShader->bindTexture("g_SkyTex", mSkyCubeTex);

				mSkyboxShader->endBinding();

				printf("skybox shader loaded!\n");

			}

			// terrain
			INST<FileService>()->readFile("Skygen/shader/Terrain_vp.glsl", vpData, vpDataLength);
			INST<FileService>()->readFile("Skygen/shader/Terrain_fp.glsl", fpData, fpDataLength);

			mTerrainVProg = INST<GfxDriver>()->createVertexProgram(vpData);
			mTerrainFProg = INST<GfxDriver>()->createFragmentProgram(fpData);
			mTerrainShader = INST<GfxDriver>()->createShader(mTerrainVProg, mTerrainFProg);

			if(mTerrainShader) {
				
				mTerrainShader->beginBinding();

				mTerrainShader->bindTexture("g_DiffMap1", mTerrainDiffTex1);
				mTerrainShader->bindTexture("g_DiffMap2", mTerrainDiffTex2);
				mTerrainShader->bindTexture("g_NoiseMap", mTerrainNoiseTex);

				mTerrainShader->endBinding();

				printf("terrain shader loaded!\n");

			}
		}

		//------------------------------

	};

}

//------------------------------------------------------------------------------
int main(int argc, char **argv) {

	using namespace Orca;

	Framework *fw = new Win32Framework;

	fw->run(new TSkygenApp, argc, argv);

	return 0;
}

//------------------------------------------------------------------------------